﻿using System;
using System.Linq;
using System.Xml;
using Microsoft.SharePoint.Administration;
using Microsoft.Web.Administration;

namespace FBA
{
    public class ConfigureMembershipProvider
    {
        /// <summary>
        /// Updates the Web Application, Central Admin and STS web.config files with the FBA settings in settingsXml.
        /// Updated nodes include:
        /// - /configuration/SharePoint/PeoplePickerWildcards
        /// - /configuration/system.web/membership/providers
        /// - /configuration/system.web/roleManager/providers
        /// - /configuration/connectionStrings
        /// Format of settingsXml needs to be as follows:
        /// <Config>
        ///    <PeoplePicker><add key="fbaMembers" value="%" /></PeoplePicker>
        ///    <SqlConnection><add name="fbaSQL" connectionString="server=yourSqlServer;database=aspnetdb;Trusted_Connection=true"/></SqlConnection>
        ///    <Membership><add name="LdapMember" type="Microsoft.Office.Server.Secu..."/></Membership>
        ///    <Role><add name="LdapRole" type="Microsoft.Office.Serve..."/></Role>
        /// </Config>
        /// </summary>
        /// <param name="webApplicationUrl"></param>
        /// <param name="zone">SPUrlZone: Custom, Default, Extranet, Intranet, Internet</param>
        /// <param name="settingsXml"></param>
        public static void Configure(string webApplicationUrl, string zone, string settingsXml)
        {
            Configure(SPWebApplication.Lookup(new Uri(webApplicationUrl)), zone, settingsXml);
        }

        public static void Configure(SPWebApplication webApplication, string zone, string settingsXml)
        {
            var spZone = (SPUrlZone)Enum.Parse(typeof(SPUrlZone), zone);
            var iisSettings = webApplication.IisSettings[spZone];

            var configWebApp = iisSettings.Path + @"\web.config";
            var configCentralAdmin = GetConfigCentralAdmin();
            var configSts = GetConfigSts();

            UpdateConfig(configCentralAdmin, settingsXml, ConfigType.CentralAdmin);
            UpdateConfig(configSts, settingsXml, ConfigType.Sts);
            UpdateConfig(configWebApp, settingsXml, ConfigType.WebApp);

        }

        private static void UpdateConfig(string configFilePath, string settingsXml, ConfigType configType)
        {
            try
            {
                var settings = new XmlDocument();
                settings.LoadXml(settingsXml);

                if (string.IsNullOrEmpty(configFilePath)) return; //Nothing to do

                var settingPeoplePicker = settings.SelectSingleNode("/Config/PeoplePicker/add");
                var settingSqlConnection = settings.SelectSingleNode("/Config/SqlConnection/add");
                var settingMembership = settings.SelectSingleNode("/Config/Membership/add");
                var settingRole = settings.SelectSingleNode("/Config/Role/add");

                var webConfigDocument = GetWebConfig(configFilePath);

                var targetConfigurationNode = webConfigDocument.SelectSingleNode("/configuration");
                if (targetConfigurationNode == null) throw new Exception("Invalid web.config. <configuration> node not found.");

                if (configType != ConfigType.Sts) EnsureFbaNodesExist(webConfigDocument, targetConfigurationNode, "SharePoint/PeoplePickerWildcards");
                EnsureFbaNodesExist(webConfigDocument, targetConfigurationNode, "system.web/membership/providers");
                EnsureFbaNodesExist(webConfigDocument, targetConfigurationNode, "system.web/roleManager/providers");
                if (settingSqlConnection != null) EnsureFbaNodesExist(webConfigDocument, targetConfigurationNode, "connectionStrings");

                if (configType != ConfigType.Sts) UpdateNode(webConfigDocument, settingPeoplePicker, "/configuration/SharePoint/PeoplePickerWildcards", "key");
                UpdateNode(webConfigDocument, settingMembership, "/configuration/system.web/membership/providers", "name");
                UpdateNode(webConfigDocument, settingRole, "/configuration/system.web/roleManager/providers", "name");
                UpdateNode(webConfigDocument, settingSqlConnection, "/configuration/connectionStrings", "name");

                webConfigDocument.Save(configFilePath);
            }
            catch (Exception exception)
            {
                throw new Exception(string.Format("Error updating {0} configuration file: {1} \r\n {2}", configType, configFilePath, exception.Message), exception);
            }
        }

        private static void EnsureFbaNodesExist(XmlDocument doc, XmlNode targetWebConfig, string xPath)
        {
            var parentNode = targetWebConfig;
            foreach (var nodeName in xPath.Split('/'))
            {
                parentNode = EnsureNodeExists(doc, parentNode, nodeName);
            }
        }

        private static XmlNode EnsureNodeExists(XmlDocument doc, XmlNode parentNode, string nodeName)
        {
            var childNode = parentNode.SelectSingleNode(nodeName);

            if (childNode == null)
            {
                childNode = doc.CreateElement(nodeName);
                parentNode.AppendChild(childNode);
            }
            return childNode;
        }

        private static XmlDocument GetWebConfig(string configFilePath)
        {
            var webConfig = new XmlDocument();
            webConfig.Load(configFilePath);
            webConfig.Save(configFilePath.ToLower().Replace("web.config", "web_" + DateTime.Now.ToString("yyyy-MM-dd-hh-mm") + "_ConfigManager.config"));
            return webConfig;
        }

        private static void UpdateNode(XmlDocument webConfig, XmlNode targetValue, string parentXPath, string attributeName)
        {
            if (targetValue == null || targetValue.Attributes == null) return;

            var str = targetValue.Attributes[attributeName].Value;
            var node = webConfig.SelectSingleNode(parentXPath);
            if(node == null) throw new Exception(string.Format("Node {0} not found in config file", parentXPath));

            var oldChild = node.SelectSingleNode("add[@" + attributeName + " = \"" + str + "\"]");
            if (oldChild == null)
            {
                node.AppendChild(webConfig.ImportNode(targetValue, true));
            }
            else
            {
                node.ReplaceChild(webConfig.ImportNode(targetValue, true), oldChild);
            }
        }

        private static string GetConfigSts()
        {
            string configSts = null;
            var manager = new ServerManager();
            var site = manager.Sites["SharePoint Web Services"];
            if (site != null)
            {
                var application2 = site.Applications.Where(vdir => (vdir.Path == "/SecurityTokenServiceApplication")).First();
                if (application2 != null)
                {
                    configSts = application2.VirtualDirectories[0].PhysicalPath + @"\web.config";
                }
            }
            return configSts;
        }

        private static string GetConfigCentralAdmin()
        {
            string configFileName = null;
            var iisSettingsWithFallback = SPWebService.AdministrationService.WebApplications.First().GetIisSettingsWithFallback(0);
            if (iisSettingsWithFallback.Path.Exists)
            {
                configFileName = iisSettingsWithFallback.Path + @"\web.config";
            }
            return configFileName;
        }
    }
}
